#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<unordered_map>
#include<cstring>
#include<sys/stat.h>
#include<fcntl.h>

#define COMMAND_SIZE 1024		// 第2步的字符数组大小

// 环境变量表使用的相关全局变量
#define MAX_ENVS 100	// 环境变量表最大数量
char* g_env[MAX_ENVS];	// 存放环境变量表的字符指针数组
int g_envs = 0;			// 环境变量表下标

// 1.获取环境变量
void InitEnv()
{
    extern char** environ;			// Linux系统为用户程序提供的环境变量表，声明一下即可访问
    memset(g_env, 0, sizeof(g_env));// 给自己的环境变量表清空
    g_envs = 0;

    // 本来要从配置文件来，此处从父进程获取
    // 1.获取环境变量
    for(int i = 0; environ[i]; i++)
    {
        // 申请空间，拷贝environ表到自己的g_env表
        g_env[i] = (char*)malloc(strlen(environ[i])+1);
        strcpy(g_env[i], environ[i]);
        g_envs++;
    }
    g_env[g_envs] = NULL;

    // 2.导成环境变量
    for(int i = 0; g_env[i]; i++)
    {
        putenv(g_env[i]);
    }
    environ = g_env;
}


// 2.打印命令行提示符
// 获取用户名
const char *GetUserName()
{
    const char *name = getenv("USER");
    return name == NULL ? "None" : name;
}
// 获取主机名
const char *GetHostName()
{
    const char *hostname = getenv("HOSTNAME");
    return hostname == NULL ? "None" : hostname;
}

char cwd[1024];
char cwdenv[1024];

// 获取当前工作目录
const char *GetPwd()
{
    // 此处做了特殊处理，getenv是从子进程获取环境变量，而我们的cd命令是内建命令，是使用父进程直接执行的，改变的是父进程的环境变量，子进程的环境变量并没有修改，所以在使用cd命令后，实际工作目录更新了，但是命令行的显示却没有更新
    // 所以此处使用getcwd来获取当前工作目录，而不从环境变量中获取工作目录
    // 获取工作目录之后，再将PWD环境变量重新编写，使用putenv写入子进程的环境变量，由此我们每次的目录就可以更新了
    // const char *pwd = getenv("PWD");
   const char *pwd = getcwd(cwd, sizeof(cwd)); //getcwd获取当前工作目录
   if(pwd != NULL)
   {
       snprintf(cwdenv,sizeof(cwdenv), "PWD=%s",cwd);
       putenv(cwdenv);
   }
    return pwd == NULL ? "None" : pwd;
}

// 为了只显示当前目录，对完整工作目录进行分割

#define FORMAT "[%s@%s %s]# "	
// 命令行提示符格式，snprintf使用,拼接的最后一个符号写成'#'和用户命令行的'$'进行区分

//截取当前目录名
std::string DirName(const char* pwd)
{
#define SLASH "/"
    std::string dir = pwd;
    if(dir == SLASH) return SLASH;
    auto pos = dir.rfind(SLASH);
    if(pos == std::string::npos) return "BUG?";
    return dir.substr(pos+1);
}

void MakeCommandLine(char cmd_prompt[], int size)
{
    //snprintf(cmd_prompt,size,FORMAT, GetUserName(), GetHostName(), GetPwd());
    snprintf(cmd_prompt,size,FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());
}

// 打印命令行提示符
void PrintCommandPrompt()
{
    char prompt[COMMAND_SIZE];
    MakeCommandLine(prompt, COMMAND_SIZE);
    printf("%s",prompt);
    fflush(stdout);
}

// 3. 获取用户输入的命令
// GetCommandLine(commandline, sizeof(commandline))	//函数调用传入的是commandline字符指针，获取输入的整行字符串
bool GetCommandLine(char* out, int size)
{
    char *c = fgets(out, size, stdin);	// fgets读取到'\n'时结束
    if(c == NULL) return false;
    out[strlen(out) - 1] = 0;			//清理\n
    if(strlen(out) == 0) return false;
    return true;
}


// 4.重定向分析
// 关于重定向的宏和全局变量
#define NONE_REDIR 0	// 代表重定向目标文件的输入输出方式
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3

int redir =  NONE_REDIR;	// 存放输入输出方式
std::string filename;		// 要重定向的的存放文件名

void TrimSpace(char cmd[], int &end)
{
    while(isspace(cmd[end]))
        end++;
}

// main函数调用RedirCheck(commandline);，传入commandline字符数组
void RedirCheck(char cmd[])
{
    redir = NONE_REDIR;	// 默认不需要重定向
    filename.clear();	// 清空文件名，因文件名是全局变量，防止下一次文件名出错
    int start = 0;
    int end = strlen(cmd) - 1;

    //"ls -a -l >> file.txt" > >> <
    // 从后往前找，找到重定向符号 或者 end走到start 为止
    while(end > start)
    {
        if(cmd[end] == '<')
        {
            cmd[end++] = 0;			// 给重定向符号成'\0'，确保cmd只能读出指令，而文件名存放在filename指针中
            TrimSpace(cmd, end);	// 去掉 重定向符号 和 文件名 之间的空格，保证filename存放的文件名正确
            redir = INPUT_REDIR;
            filename = cmd + end;	// filename指针指向文件名的头
            break;
        }
        else if(cmd[end] == '>')
        {
            if(cmd[end - 1] == '>')
            {
                cmd[end - 1] = 0;
                redir = APPEND_REDIR;
            }
            else
            {
                redir = OUTPUT_REDIR;
            }
            cmd[end++] = 0;
            TrimSpace(cmd, end);
            filename = cmd + end;// filename指针指向文件名的头
            break;
        }
        else
        {
            end--;
        }
    }
}

// 5.命令行分割
// 命令行参数表
#define MAXARGC 128
char *g_argv[MAXARGC];
int g_argc = 0;

// 命令行分析(分割) "ls -a -l" -> "ls" "-a" "-l"
bool CommandParse(char *commandline)
{
#define SEP " "
    g_argc = 0;
    // 命令行分析"ls -a -l" -> "ls" "-a" "-l"
    g_argv[g_argc++] = strtok(commandline, SEP);		//strtok(字符指针，分割符);
    //第>1次使用strtok字符指针位置传NULL则会在上次结束位置继续进行分割
    while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
    g_argc--;	// strtok找不到对应分割符会返回NULL,g_arg--使g_argv数组忽略掉strtok的最后一个返回值NULL
    return g_argc > 0;
}


// 6.检测并处理内建命令
// cd内建命令执行函数
// 1.cd后不跟选项，跳转到家目录
// 2.cd后跟'-'跳转到上次的目录
// 3.cd后跟'~'跳转到家目录
// 4.cd后跟据具体目录，跳转到指定目录
// 
const char *GetHome()
{
    const char *home = getenv("HOME");
    return home == NULL ? "" : home;
}

bool Cd()
{
    if(g_argc == 1)
    {
        std::string home = GetHome();
        if(home.empty()) return true;
        chdir(home.c_str());    // 改变当前进程的目录
    }
    else
    {
        std::string where = g_argv[1];
        if(where == "-")
        {
			//自己摸索吧少年！！
        }
        else if(where == "~")
        {
            std::string home = GetHome();
            if(home.empty()) return true;
            chdir(home.c_str());    // 改变当前进程的目录
        }
        else 
        {
            chdir(where.c_str());
        }
    }
}

// echo内建命令执行函数
// 1.echo "字符串"    打印字符串
// 2.echo $? 		 打印上一个进程的退出状态
// 3.echo $环境变量名  打印环境变量名对应的内容

// 退出状态全局变量 last exit code
int lastcode = 0;

bool Echo()
{
    if(g_argc == 2)
    {
        // echo "helloe world"
        // echo $?
        // echo $PATH
        std::string opt = g_argv[1];
        if(opt == "$?")
        {
            std::cout << lastcode << std::endl;
            lastcode = 0;
        }
        else if(opt == "$")
        {
            std::string env_name = opt.substr(1);
            const char *env_value = getenv(env_name.c_str());
            if(env_value)
                std::cout << env_value << std::endl;
        }
        else
        {
            std::cout << opt << std::endl;
        }
    }
}

// 内建命令调用函数
bool CheckAndExecBuiltin()
{
    std::string cmd = g_argv[0];
    if(cmd == "cd")
    {
        Cd();
        return true;
    }
    else if(cmd == "echo")
    {
        Echo();
        return true;
    }
    else if(cmd == "export")
    {
        // Export();
        return true;
    }
    else if(cmd == "alias")
    {
        // Alias();
        return true;
    }
    return false;
}

// 7.执行命令
int Execute()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        // 1.处理有重定向的命令，根据redir在4.5第4步中得到的值，并使用dup2函数实现重定向处理
        int fd = -1;
        if(redir == INPUT_REDIR)
        {
            fd = open(filename.c_str(), O_RDONLY);
            if(fd < 0)  exit(1);
            dup2(fd, 0);
            close(fd);
        }
        else if(redir == OUTPUT_REDIR)
        {
            fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666);
            if(fd < 0)  exit(1);
            dup2(fd, 1);
            close(fd);
        }
        else if(redir == APPEND_REDIR)
        {
            fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
            if(fd < 0)  exit(1);
            dup2(fd, 1);
            close(fd);
        }
        
		// 2.有重定向则先处理重定向再进行进程替换，没有重定向则直接进程替换。
        // 进程替换，把子进程替换成shell处理指令的操作
        execvp(g_argv[0],g_argv);	
        exit(1);
    }
    // father，父进程阻塞等待回收子进程
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
        lastcode = WEXITSTATUS(status);
    }
    return 0;
}

int main()
{
    // 1.shell启动的时候，从系统中获取环境变量
    // 我们的环境变量信息应该从父shell统一来
    InitEnv();
    while(true)
    {
        // 2.打印命令行提示符
        PrintCommandPrompt();
        
        // 3.获取用户输入的命令
        char commandline[COMMAND_SIZE];
        if(!GetCommandLine(commandline, COMMAND_SIZE))
            continue;

        // 4.重定向分析"ls -a -l > file.txt" -> "la -a -l" "file.txt" -> 判定重定向方式
        RedirCheck(commandline);

        // 5.命令行分析(分割) "ls -a -l" -> "ls" "-a" "-l"
        CommandParse(commandline);
    
        // 6.检测并处理内建命令
        if(CheckAndExecBuiltin())
            continue;

        // 7.执行命令
        Execute();
    }
    return 0;
}
